﻿using System;
using System.Diagnostics;
using System.Threading.Tasks;
#if NETFX_CORE || WINDOWS_PHONE
using System.Reflection;
using Microsoft.VisualStudio.TestPlatform.UnitTestFramework;
#else
using Microsoft.VisualStudio.TestTools.UnitTesting;
#endif

namespace Storage.More.Test
{
   /// <summary>
   /// Provides a similar "expected exception" mechanism for both Windows unit tests and Windows Store App unit tests.
   /// This approach is preferred because it allows more precise targetting over where the exception is expected.
   /// </summary>
   [DebuggerNonUserCode]
   public static class AssertHelper
   {
      /// <summary>
      /// Executes an action that is expected to throw a certain type of exception.
      /// </summary>
      /// <typeparam name="TException">Expected exception type.</typeparam>
      /// <param name="action">Action that is expected to throw the exception.</param>
      /// <param name="allowDerivedException">True to allow derived exception types in addition to the specified type; false to allow only the specified type.</param>
      /// <param name="message">Descriptive message used when the assertion fails.</param>
      public static void ThrowsException<TException>(Action action, bool allowDerivedException = false, string message = null)
         where TException : Exception
      {
         message = message != null ? " " + message : "";

         var threw = false;
         try
         {
            action();
         }
         catch (Exception e)
         {
            threw = true;
            if (!IsAcceptable<TException>(e, allowDerivedException))
               Assert.Fail("Expected<{0}> Actual<{1}>.{2}", typeof(TException).Name, e.GetType().Name, message);
         }
         finally
         {
            if (!threw)
               Assert.Fail("Expected<{0}> Actual<None>.{1}", typeof(TException).Name, message);
         }
      }

      /// <summary>
      /// Executes an action that is expected to throw a certain type of exception.
      /// </summary>
      /// <typeparam name="TException">Expected exception type.</typeparam>
      /// <param name="func">Action that is expected to throw the exception.</param>
      /// <param name="allowDerivedException">True to allow derived exception types in addition to the specified type; false to allow only the specified type.</param>
      /// <param name="message">Descriptive message used when the assertion fails.</param>
      public async static Task ThrowsException<TException>(Func<Task> func, bool allowDerivedException = false, string message = null)
         where TException : Exception
      {
         message = message != null ? " " + message : "";

         var threw = false;
         try
         {
            await func();
         }
         catch (Exception e)
         {
            threw = true;
            if (!IsAcceptable<TException>(e, allowDerivedException))
               Assert.Fail("Expected<{0}> Actual<{1}>.{2}", typeof(TException).Name, e.GetType().Name, message);
         }
         finally
         {
            if (!threw)
               Assert.Fail("Expected<{0}> Actual<None>.{1}", typeof(TException).Name, message);
         }
      }

      static bool IsAcceptable<TException>(Exception actual, bool allowDerived)
      {
#if NETFX_CORE
         var expectedType = typeof(TException).GetTypeInfo();
         var actualType = actual.GetType().GetTypeInfo();
#else
         var expectedType = typeof(TException);
         var actualType = actual.GetType();
#endif

         bool acceptable;
         if (allowDerived)
            acceptable = expectedType.IsAssignableFrom(actualType);
         else
            acceptable = expectedType.Equals(actualType);

         return acceptable;
      }
   }
}
